#!/usr/bin/perl -wT

########################
#
# Perl Modules
#
########################

use strict;
use Getopt::Easy;
use NetAddr::IP;
use Text::ASCIITable 0.20; #0.18 is WAY to slooow
use Date::Format;
use Net::DNS;

########################
#
# Global Variables
#
########################

# Keeps track of what section we are working in for the config file
our $tracker = 'none';

# Hold the address objects
our %address_objects;

# Hold the group address objects
our %group_objects;

# hold the policy objects
our @policy_objects;

# hold the target ip object we are looking for
our $target_ip;
our $target_scope;

# hold the list of matched policies
our @policy_hits;

# hold the hostname of the firewall
our $fw_hostname = "";

# hold the management ip of the firewall
our $fw_mgmt_ip = "";

our $dns   = Net::DNS::Resolver->new;
$dns->retry(1);
$dns->udp_timeout(30);


#########################
#
# Subroutine Definitions
#
#########################

# List the available options to pass in
get_options     ("e-expand E-bothforms f-filename= h-help t-targetip= z-targetzone= F-format= D-debug w-within d-destination-only s-source-only", &get_help);

sub add_any_objects {
        # Add the 'Any' object in each zone to the list of group and address objects, otherwise we don't catch the 'Any' rules, only those with actual object names
        foreach my $zone (keys %address_objects) {
                # hash setup: $address_objects{'zone'}{'objectname'}{'address|netmask'} = <address|netmask>
                # Creates an address object named 'Any', which can be pointed to by a group
                $address_objects{$zone}{'Any'} = new NetAddr::IP ('0.0.0.0', '0.0.0.0');
                $address_objects{$zone}{'Any-IPv4'} = new NetAddr::IP ('0.0.0.0', '0.0.0.0');
                $address_objects{$zone}{'Any-IPv6'} = new NetAddr::IP ('::/0');
        
                # hash setup: $group_objects{'zone'}{'groupname'}[arrayindex] = <objectname>
                # this is a hash of hashes containing arrays - yikes!
                # This particular entry for each zone adds a group object named 'Any' which points to an address object named 'Any'
                push(@{$group_objects{$zone}{'Any'}},'Any');
                push(@{$group_objects{$zone}{'Any-IPv4'}},'Any-IPv4');
                push(@{$group_objects{$zone}{'Any-IPv6'}},'Any-IPv6');

                &print_debug("Added 'Any*' objects to zone $zone");

        }
}

# Expands a group or address objet 
# expand_object($object, $zone)
sub expand_object {

        # track the addresses we find
        my @addresses;

        # arguments
        my $object = $_[0];
        my $zone= $_[1];

        &print_debug("expanding '$object' from $zone");
        my $object_addresses_ref = [];
        if (defined($group_objects{$zone}{$object})) {
                $object_addresses_ref = $group_objects{$zone}{$object};
        } elsif (defined($address_objects{$zone}{$object})) {
                $object_addresses_ref = [$object];
        }

        # Try to match an address object
        foreach my $address (@{$object_addresses_ref}) {

                #my $cidr = $address_objects{$zone}{$address};
                #&print_debug("Creating a new NetAddr::IP from '$cidr'");
                #my $ip = new NetAddr::IP ($cidr);
                #my $ip = $address_objects{$zone}{$address};
                my @ips = ();
                if (defined $address_objects{$zone}{$address}) {
                    @ips = ($address_objects{$zone}{$address});
                } elsif (defined $group_objects{$zone}{$address}) {
                    #handle recursive groups recurively 
                    @ips = &expand_object($address,$zone);
                }

                for my $ip (@ips) {
                    &print_debug("expanding '$address' as $ip IPv" . $ip->version());
                   
                    if(not defined $ip) {
                         die "Undefined IP for $address included in $object from $zone"
                    }
             

                    if($O{'within'}){
                        #narrow down target zone matches
                        if(lc($zone) eq lc($O{'targetzone'})) {
                            push(@addresses,$ip) if $target_ip->contains($ip);
                        }
                        elsif(lc($zone) eq 'global') {
                            push(@addresses,$ip) if $target_ip->contains($ip);
                        }
                        else {
                            #do full expansion for non-zonetarget zones
                            push(@addresses,$ip);
                        }
                           
                    } else {
                        push(@addresses,$ip);
                    }
            
                    &print_debug("Added $ip to address object $object in zone $zone.");
                }

        }
        
        &print_debug("Found " . scalar(@addresses) . " address(es) for $object in zone $zone.");


        return @addresses;
}

# function to store the firewall hostname
# get_fw_hostname($configline)
sub get_fw_hostname {

        #collect the line element
        ($fw_hostname) = ($_[0] =~ /^\s*set hostname (.*)$/); 

        &print_debug("Found hostname $fw_hostname.");

}       

# function to store the firewall management IP address
# get_fw_mgmt_ip($configline)
sub get_fw_mgmt_ip {

        ($fw_mgmt_ip) = ($_[0] =~ /^\s*set interface mgt ip (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2})/);

        &print_debug("Found management IP $fw_mgmt_ip.");

}


# Function to generate usage display text
sub get_help {

        my $help = <<'EOF';

        Usage: expand-juniper.pl [-d] [-e] [-E] [-F <format>] [-h] [-z <targetzone>] -f <filename> -t <targetobj> [-w] [-s] [-d]
        -D      Turn on debugging to STDERR.
        -e      Expand all group and address object to CIDR notation.
        -E      Outputs both the compact and expanded forms of the reports.
        -F      Specify the output format. Currently supports 'csv', 'html', and 'table'.
        -h      Help; Show this message.
        -z      The name of the security zone where the target IP (-t) is located,
                otherwise the script assumes that the IP can be reached through any zone.
                This option is recommended, otherwise all 'Any' entries will match.
        -f      The relative path to the filename for processing.
        -t      The target IP address or CIDR subnet to be used for policy matching.
                Examples:       192.168.10.4 (single IP, assumed /32 mask)
                                192.168.10.0/27 (entire /27 range must be matched)
                                192.168.10.0/255.255.255.224 (same as using /27)
                                fe80::/64 (IPv6)
                                any or 0.0.0.0/0 (will match all policies)
        -w      Matches <targetobj> or anything within <targetobj>.
                For example -t 192.168.10.0/24 -w will match a rule using specifically 192.168.10.99
        -s      Limit matches to policy source addresses (default is both source and destination)
        -d      Limit matches to policy destination addresses

                Initially written by James Schneider (jrschneider@csupomona.edu) , January 2010
                Patch contribution by James M.
EOF
        return $help;

}

sub get_html_footer {

        return ""; #JM

        my $footer;

        $footer .= "</body>\n";
        $footer .= "</html>";

        return $footer;
}

# retrieve the html header information for html output
sub get_html_header {
        return "";

        # get the report generation date
        my $date = time2str('%B %e, %Y %X %Z %z', time, 'GMT');

        &print_debug("Report generation time $date.");

        # generate the banner title
        my $target_zone = $O{'targetzone'} ? " - Zone: $O{'targetzone'}" : "";

        # generate the table title
        my $title = "Juniper Policy Matches for $target_ip$target_zone - $date";

        my $header =<< 'EOF';

        <!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">
        <html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\">
        <head>
                <title>$title</title>
                <style type=\"text/css\" media=\"all\">
                
                        #hor-zebra
                        {
                                font-family: \"Lucida Sans Unicode\", \"Lucida Grande\", Sans-Serif;
                                font-size: 12px;
                                margin-left: auto;
                                margin-right: auto;
                                margin-top: 15px;
                                margin-bottom: 50px;
                                width: 850px;
                                text-align: left;
                                border-collapse: collapse;
                                border: 1px solid lightgray;
                        }
                
                        #hor-zebra th
                        {
                                font-size: 14px;
                                font-weight: normal;
                                padding: 10px 8px;
                                color: #039;
                                text-align: left;
                                border-collapse: collapse;
                                border-left: 1px solid lightgray;
                                border-right: 1px solid lightgray;
                        }
                
                        #hor-zebra td
                        {
                                padding: 8px;
                                color: #669;
                                vertical-align: top;
                                border-collapse: collapse;
                                border-left: 1px solid lightgray;
                                border-right: 1px solid lightgray;
                        }
                
                        #hor-zebra .odd
                        {
                                background: #e8edff;
                        }
                
                </style>
        </head>
        <body>
        
EOF

        return $header;

}
        
#search for policy hits within the policies
sub get_policy_hits {
        foreach my $hash (@policy_objects) {

                my $found_match = 0;
                foreach my $sourceaddr (@{$hash->{'sourceaddr'}}) {
                        next if $O{'destination-only'}; #skip sources if not interested
                        # if we find a match in the destination address, store 
                        if (&target_search($sourceaddr, $hash->{'sourcezone'})) {
                                $found_match = 1;
                                last;
                        }
                }

                foreach my $destaddr (@{$hash->{'destaddr'}}) {
                        next if $O{'source-only'}; #skip destinations if not interested
                        # if we find a match in the destination address, store 
                        if (&target_search($destaddr, $hash->{'destzone'})) {
                                $found_match = 1;
                            last;
                        }
                }

                if($found_match) {
                   push (@policy_hits, $hash->{'id'});
                   &print_debug("Policy hit found, rule ID $hash->{'id'}.");
                }
        }
}

# prints debug statements if requested
# print_debug($message)
sub print_debug {
        if(not $O{'debug'}) { return }

        my $line = $_[0];
        chomp($line);
        $| = 1;  #autoflush
        print STDERR ("DEBUG: $line\n");
}

# Function to print any policy matches found
sub print_matches {

        # hold the csv version
        my @csv;
        
        # hold the table version
        my $table; 
        
        # get the report generation date
        my $date = time2str('%B %e, %Y %X %Z %z', time, 'GMT');

        my $title = "Policy Matches for $target_ip";
        if($O{'within'}) {
           $title = "Policy Matches within $target_ip";
        }
      

        if ($O{'format'} eq 'csv' or $O{'format'} eq 'html') {
                push(@csv,['Rule','Policy','From Zone', 'Source', 'To Zone', 'Destination','Service', 'Action']);
        }
        else {
                # add the date to the title
                #$title .= "\n$date";

                # add the firewall hostname
                #$title .= "\nFirewall Hostname: $fw_hostname" if $fw_hostname;

                # add the firewall mgmt ip
                #$title .= "\nFirewall Management IP: $fw_mgmt_ip" if $fw_mgmt_ip;

                # generate the banner title
                #$title .= $O{'targetzone'} ? "\nZone: $O{'targetzone'}" : "";

                #$title .= $O{'expand'} ? "\nExpanded Format" : "\nCompact Format";

                # generate the table title
                $table = Text::ASCIITable->new({ headingText => $title });

                # setup the columns for the table
                $table->setCols('Rule','Policy','From', 'Source', 'To', 'Destination','Service', 'Action');

        }

        # now we cycle through each of the policy hits and add them to the table
        foreach my $policy_id (@policy_hits) {

                # loop through all the policy objects and look for matching policy ids
                # this way we can pull all the information we need from each matching policy
                for (my $i = 0; $i < scalar(@policy_objects); $i++) {

                        # find the matches on the policy ids 
                        if ($policy_objects[$i]{'id'} eq $policy_id) {

                                # if expansion on the addresses is requested
                                if ($O{'expand'}) {

                                        my @temp_src_addr = ();                                         
                                        my @temp_dst_addr = ();
                                        
                                        # do the expansion of the source group/address objects
                                        foreach my $address (@{$policy_objects[$i]{'sourceaddr'}}) {

                                                push(@temp_src_addr, &expand_object($address, $policy_objects[$i]{'sourcezone'}));
                                        }

                                        # do the expansion of the destination group/address objects
                                        foreach my $address (@{$policy_objects[$i]{'destaddr'}}) {

                                                push(@temp_dst_addr, &expand_object($address, $policy_objects[$i]{'destzone'}));
                                        }
                                        
                                        # csv format
                                        if ($O{'format'} eq 'csv') {

                                                my $src_addr_list = join(";",sort(@temp_src_addr));
                                                my $dst_addr_list = join(";",sort(@temp_dst_addr));
                                                my $service_list = join(";",sort(@{$policy_objects[$i]{'service'}}));
                                                
                                                push(@csv,[$i+1,$policy_id,$policy_objects[$i]{'sourcezone'},$src_addr_list,$policy_objects[$i]{'destzone'},$dst_addr_list,$service_list,$policy_objects[$i]{'action'}]);
                                        }
                                        # html format
                                        elsif ($O{'format'} eq 'html') {

                                                my $src_addr_list = join("</div><div>",sort(@temp_src_addr));
                                                my $dst_addr_list = join("</div><div>",sort(@temp_dst_addr));
                                                my $service_list = join("</div><div>",sort(@{$policy_objects[$i]{'service'}}));
                                                
                                                push(@csv,[$i+1,$policy_id,$policy_objects[$i]{'sourcezone'},$src_addr_list,$policy_objects[$i]{'destzone'},$dst_addr_list,$service_list,$policy_objects[$i]{'action'}]);
                                        }
                                        # otherwise assume ascii table format
                                        else {
                                                
                                                my $src_addr_list = join("\n",map {lc($_->cidr())} sort(@temp_src_addr));
                                                my $dst_addr_list = join("\n",map {lc($_->cidr())} sort(@temp_dst_addr));
                                                my $service_list = join("\n",sort(@{$policy_objects[$i]{'service'}}));

                                                # tables can take arrays with elements separated by "\n"
                                                $table->addRow($i+1,$policy_id,$policy_objects[$i]{'sourcezone'},$src_addr_list,$policy_objects[$i]{'destzone'},$dst_addr_list,$service_list,$policy_objects[$i]{'action'});
                                        }
                                        
                                }
                                # no expansion requested, everything is printed as it is in the config file
                                else {
                                        # csv format
                                        if ($O{'format'} eq 'csv') {

                                                push(@csv,[$i+1,$policy_id,$policy_objects[$i]{'sourcezone'},join(";",@{$policy_objects[$i]{'sourceaddr'}}),$policy_objects[$i]{'destzone'},join(";",@{$policy_objects[$i]{'destaddr'}}),join(";",@{$policy_objects[$i]{'service'}}),$policy_objects[$i]{'action'}]);

                                        }
                                        # html format
                                        elsif ($O{'format'} eq 'html') {

                                                push(@csv,[$i+1,$policy_id,$policy_objects[$i]{'sourcezone'},join("</div><div>",@{$policy_objects[$i]{'sourceaddr'}}),$policy_objects[$i]{'destzone'},join("</div><div>",@{$policy_objects[$i]{'destaddr'}}),join("</div><div>",@{$policy_objects[$i]{'service'}}),$policy_objects[$i]{'action'}]);

                                        }
                                        else {
                                                $table->addRow($i+1,$policy_id,$policy_objects[$i]{'sourcezone'},join("\n",@{$policy_objects[$i]{'sourceaddr'}}),$policy_objects[$i]{'destzone'},join("\n",@{$policy_objects[$i]{'destaddr'}}),join("\n",@{$policy_objects[$i]{'service'}}),$policy_objects[$i]{'action'});
                                        }
                                }

                                # add a delimiter in the table between policies
                                $table->addRowLine() unless $O{'format'} eq 'csv' or $O{'format'} eq 'html';
                        }
                }

        }

        # if we're printing a csv file
        if ($O{'format'} eq 'csv') {

                foreach my $row (@csv) {
                        print join(",",@$row);
                        print "\n";
                }
        }
        # if we're printing html output
        elsif ($O{'format'} eq 'html') {

                # Get the initial format of the page
                print (&get_html_header);

                # loop tracking variable, so we can treat the header row differently
                my $first_run = 'true';

                # set the zebra stripe
                my $zebra = 0;

                # print the header table for the data table
                #print "<table id=\"hor-zebra\" summary=\"Juniper Policy Matches\" style=\"text-align: center; margin-top: 50px; margin-bottom: 15px;\">\n";
                #print "\t<thead>\n";
                #print "\t\t<tr>\n";
                #print "\t\t\t<th style=\"text-align: center;\">$title</th>\n";
                #print "<tr><th colspan=\"8\" style=\"padding-top: 2em;\">$title</th></tr>\n";
                print "<tr><th colspan=\"8\"><h2>$title</h2></th></tr>\n";
                #print "\t\t</tr>\n";
                #print "\t</thead>\n";
                #print "\t<tbody\n";
                #print "\t\t<tr>\n";
                #print "\t\t\t<td>$date</td>\n";
                #print "\t\t</tr>\n";
                #print "\t\t<tr>\n";
                #print "\t\t\t<td>Firewall Hostname: $fw_hostname</td>\n";
                #print "\t\t</tr>\n";
                #print "\t\t<tr>\n";
                #print "\t\t\t<td>Firewall Management IP: $fw_mgmt_ip</td>\n";
                #print "\t\t</tr>\n";
                #if ($O{'targetzone'}) {
                #        print "\t\t<tr>\n";
                #        print "\t\t\t<td>Zone: $O{'targetzone'}</td>\n";
                #        print "\t\t</tr>\n";
                #}
#
#                if ($O{'expand'}) {
#                        print "\t\t<tr>\n";
#                        print "\t\t\t<td>Expanded Format</td>\n";
#                        print "\t\t</tr>\n";
#                }
#                else {
#                        print "\t\t<tr>\n";
#                        print "\t\t\t<td>Compact Format</td>\n";
#                        print "\t\t</tr>\n";
#                }

#                print "</table>\n";


                # now print each of the result rows
                foreach my $row (@csv) {

                        if ($first_run) {

                                #print the heading for the table
                                #print "<table id=\"hor-zebra\" summary=\"Juniper Policy Matches\">\n";
                                print "\t<thead>\n";
                                print "\t\t<tr>\n";
                
                                foreach my $k (@$row) {
                                        print "\t\t\t<th scope=\"col\">$k</th>\n";
                                }

                                # print the footer of the column header
                                print "\t\t</tr>\n";
                                print "\t</thead>\n";
                                print "\t<tbody>\n";

                                # first run is now false
                                $first_run = 0;
                                next;
                
                        }

                        # check for zebra stripe
                        if ($zebra % 2) {
                                print "\t\t<tr>\n";
                        }
                        else {
                                print "\t\t<tr class=\"odd\">\n";
                        }

                        # increment the zebra striper
                        $zebra++;

                        # now print each rows data
                        foreach my $k (@$row) {
                                print "\t\t\t<td><div>$k</div></td>\n";
                        }

                        print "\t\t</tr>\n";

                }

                print "\t</tbody>\n";
                #print "</table>\n";

                # close off the code
                unless ($O{'bothforms'}) { print (&get_html_footer); }

        }
        # otherwise print a table
        else {

                # now print the entire table
                print $table;
        }
}

# function to search for a match of a particular ip address within a subnet passed in
# target_search($groupname, $zone_name)
# returns true or false depending on whether or not the target ip is contained in this particular group
sub target_search {

        my $object = $_[0];
        my $zone = $_[1];

        # return true if 'any' or '0.0.0.0/0' specified as target ip ($target_ip is a NetAddr::IP object, not what was passed in)
        #if ($target_ip eq '0.0.0.0/0') { return 'true'; }

        my @address_names = ();
        if (defined($address_objects{$zone}{$object})) {
           push @address_names, $object
        }
        if (defined($group_objects{$zone}{$object})) {
                foreach my $address (@{$group_objects{$zone}{$object}}) {
                     push @address_names, $address
                }
        }
             
        
        foreach my $address_name (@address_names) {
            #my $cidr = $address_objects{$zone}{$address_name};
            #&print_debug("Creating a new NetAddr::IP from '$cidr'");
            #my $ip = new NetAddr::IP ($cidr);
            my $ip = $address_objects{$zone}{$address_name};
            if (not defined $ip) {
                &print_debug("Trouble creating IP from '$address_name'");
                next;
            }

            #mostly to keep v6 'any' from mathing v4 'any'
            next unless $ip->version() eq $target_ip->version();
            

            if($address_name eq 'Any-IPv4')         {
                &print_debug("Testing $target_ip against Any-IPv4 in $zone");
            }
            if($O{'within'}){
                #check to see if a rule target is with the scope of -t
                next unless $target_ip->contains($ip);
                &print_debug("$target_ip contains $address_name");
            } else {
                #check to see target is contained within a rule target
                next unless $ip->contains($target_ip);
                &print_debug("$address_name contains $target_ip");
            }
            

            # Match zone if -z is defined
            if( lc($O{'targetzone'}) eq lc($zone)) {
                 &print_debug("$address_name contains $target_ip and $O{'targetzone'} matches $zone");
                 return 'true';
            }

            # Match zone if Global 
            if( lc($zone) eq "global") {
                 &print_debug("$address_name contains $target_ip in Global zone");
                 return 'true';
            }

            # Always allows a true return if -z is not defined
            if(not defined $O{'targetzone'}) {
                 return 'true';
            }
             
            #if here, target matched address, but not zone
            &print_debug("address $target_ip matched policy, but outside of targetzone $zone ne $O{'targetzone'}");
        }
        
        # Returns false since we didn't get any hits
        return 0;

}               

# add an address to the list
sub tracker_address {

        my $line = $_[0];
        chomp($line);
        return unless defined $line;


        #collect the line elements
        #set address "inside" "2607:f380:a61::/48" 2607:f380:a61::/48
        #set address "user" "2607:f380:a61::/48" 2607:f380:a61::/48
        #

        #v4 specific
        #my @lineparts = ($line =~ /^\s*set address "(.*?)" "(.*?)" (\d+\.\d+\.\d+\.\d+) (\d+\.\d+\.\d+\.\d+)/); 

        #                                                             v4host                mask               | v6/mask         | DNS 
        my @lineparts = ($line =~ /^\s*set address "(.*?)" "(.*?)" (?:(\d+\.\d+\.\d+\.\d+) (\d+\.\d+\.\d+\.\d+)|([a-f0-9:]+\/\d+)|(\S+))/); 

        if (scalar(@lineparts) < 4){
             &print_debug("bad parse of address line $line.: " .  join(",",@lineparts));
             return;
        }

        #&print_debug("lineparts: " . join(", ", @lineparts));

        if (defined($lineparts[2])){
            $address_objects{$lineparts[0]}{$lineparts[1]} = new NetAddr::IP($lineparts[2] , $lineparts[3]);
            &print_debug("tracking: $address_objects{$lineparts[0]}{$lineparts[1]}")

        }
        if (defined($lineparts[4])){
           #$address_objects{$lineparts[0]}{$lineparts[1]} =  $lineparts[4];              
           $address_objects{$lineparts[0]}{$lineparts[1]} = new NetAddr::IP($lineparts[4]);
           &print_debug("tracking: $address_objects{$lineparts[0]}{$lineparts[1]}")
        }
        if (defined($lineparts[5])){
            #warn "can't do DNS resolution yet, sorry - $lineparts[5]";
            my $query = $dns->search($lineparts[5]);
            if ($query) {
                  $group_objects{$lineparts[0]}{$lineparts[1]} = [];
                  foreach my $rr ($query->answer) {
                      next unless $rr->type =~ /^A|AAAA$/;
                      #print $rr->address, "\n";
                      #$address_objects{$lineparts[0]}{$lineparts[1]} = new NetAddr::IP($rr->address);
                      $address_objects{$lineparts[0]}{$rr->address} = new NetAddr::IP($rr->address);
                      &print_debug("dns tracking $lineparts[5] ('$lineparts[1]' from '$lineparts[0]') as '". scalar($rr->address) ."' ($address_objects{$lineparts[0]}{$rr->address})");
                      push(@{$group_objects{$lineparts[0]}{$lineparts[1]}},$rr->address);
                  }
                  if(scalar(@{$group_objects{$lineparts[0]}{$lineparts[1]}}) < 1) {
                      &print_debug("dns tracking punt $lineparts[0] $lineparts[1]");
                      push(@{$group_objects{$lineparts[0]}{$lineparts[1]}},'Any');
                  }
              } else {
                  warn "query failed: ", $dns->errorstring, " for $lineparts[5]\n";
              }

             
        }
       

        if (not defined $address_objects{$lineparts[0]}{$lineparts[1]} and not defined $lineparts[5]){
             warn "bad? address line to object: $lineparts[0] -> $lineparts[1]"
        }
   
        
}

# add a group to the list
sub tracker_groupaddress {

        my $line = $_[0];
                
        #collect the line elements
        my @lineparts = ($line =~ /^\s*set group address "(.*?)" "(.*?)" add "(.*?)"/); 
        for (@lineparts) {
           die qq{tracker_groupaddress couldn't parse "$line"} if not $_;
        }

        my ($zone,$group,$address) = @lineparts;

        # hash setup: $group_objects{'zone'}{'groupname'}[arrayindex] = <objectname>
        # this is a hash of hashes containing arrays - yikes!
        push(@{$group_objects{$zone}{$group}},$address) unless !@lineparts;

}

# add the initial policy line to the list of policies
sub tracker_policy {

        my $line = $_[0];               

        #collect the line elements
        my @lineparts = ($line =~ /^\s*set policy (?:global )?id (\d+).*?from "(.*?)" to "(.*?)"  "(.*?)" "(.*?)" "(.*?)" ((?:\S+ )*(?:permit|deny|tunnel))/); 
        for (@lineparts) {
           die qq{tracker_policy couldn't parse "$line"} if not $_;
        }

        # Populate a hash with the template for the initial batch of info for this policy
        my %ruleinfo = (
                        'id'         => $lineparts[0],
                        'sourcezone' => $lineparts[1],
                        'destzone'   => $lineparts[2],
                        'sourceaddr' => [$lineparts[3]], #Array
                        'destaddr'   => [$lineparts[4]], #Array
                        'service'    => [$lineparts[5]], #Array
                        'action'     => $lineparts[6] 
        );

        # array to keep all the policy objects, in order
        # array setup: $policy_objects[arrayindex]{hashkey}
        # note that the array index also represents the order of the policies, in ascending order (ie element 0 is processed first on the device)
        # also note that 3 of the elements are pointers to other arrays, as noted above
        push(@policy_objects,\%ruleinfo);

}

# add the additional policy rules to the policy
sub tracker_policyrules {

        my $line = $_[0];

        # grab the policy number we are on, less one for array referencing
        my $policy_num = scalar(@policy_objects) - 1;

        # if we reach an exit line, stick a fork in this policy, its done...
        if ($line =~ /\s*exit/) { 
                $tracker = 'none';
        }

        # look for service references in this policy
        elsif ($line =~ /\s*set service "(.*?)"/) {
                
                push(@{$policy_objects[$policy_num]{'service'}}, $1);
        }

        # look for source address references in this policy
        elsif ($line =~ /\s*set src-address "(.*?)"/) {
                
                push(@{$policy_objects[$policy_num]{'sourceaddr'}}, $1);
        }

        # look for destination address references in this policy
        elsif ($line =~ /\s*set dst-address "(.*?)"/) {
                
                push(@{$policy_objects[$policy_num]{'destaddr'}}, $1);
        }
}

########################
#
# Main Program
#
########################

&print_debug("debug (-d) is enabled");

# if -h is passed, print help and exit
if ($O{'help'}) { print &get_help; exit; }

# if no -t is passed, print error, help, and exit
if (!$O{'targetip'}) { 
        print "ERROR: Target IP address must be specified. (-t)\n"; 
        print &get_help; 
        exit; 
}
$target_ip = new NetAddr::IP ($O{'targetip'});
&print_debug("Target IP given is $target_ip");


# check if the filename has been passed in, otherwise print an error and exit
if (!$O{'filename'}) { die "ERROR: Filename (-f) argument required.\n" . &get_help; }

&print_debug("Filename to use is $O{'filename'}");

if ($O{'format'}) { 
        &print_debug("Requested format: $O{'format'}"); 
}
else {
        &print_debug("Requested format: table"); 
}

if ($O{'bothforms'}) { 
        &print_debug("The expanded and compact reports have both been requested.");
}
elsif ($O{'expand'}) {
        &print_debug("The expanded report has been requested.");
}
else {
        &print_debug("The compact report been requested.");
}

if ($O{'targetzone'}) { 
        &print_debug("The target zone specified is $O{'targetzone'}.");
}
else {
        &print_debug("No target zone specified.");
}

&print_debug("Now attempting to parse the configuration file $O{'filename'}.");

# attempt to open the file passed on the command line for parsing
open(CONFIGFILE, $O{'filename'}) or die("Unable to open file $O{'filename'}");

 # now loop through the file
while (<CONFIGFILE>) {

        chomp;
        if ($tracker eq 'policyrules')          { &tracker_policyrules($_); &print_debug ("matched line: $.: $_"); }
        elsif (/^\s*set address/)               { &tracker_address($_); &print_debug ("matched line: $.: $_"); }
        elsif (/^\s*set group address .* add/)  { &tracker_groupaddress($_); &print_debug ("matched line: $.: $_"); }
        elsif (/^\s*set policy (?:global )?id.*from/)   { &tracker_policy($_); $tracker = 'policyrules'; &print_debug ("matched line: $.: $_"); }
        elsif (/^\s*set hostname.*/)            { &get_fw_hostname($_); &print_debug ("matched line: $.: $_"); }
        elsif (/^\s*set interface mgt ip.*/)    { &get_fw_mgmt_ip($_); &print_debug ("matched line: $.: $_"); }
        else                                    { &print_debug ("unknown line: $.: $_") }

}

# add in the special case 'any' source and destination objects since those aren't in the config
&add_any_objects;

# run through and track all the policy hits
&get_policy_hits;                       

# if both expanded and compact forms are requested, print the compact first
if ($O{'bothforms'}) {

        &print_debug("Both the compact and expanded forms will be generated.");

        # turn expansion off if it was turned on
        $O{'expand'} = 0;
        
        # print the unexpanded matches
        &print_matches;

        # add some spacing between the reports for anything not html
        if ($O{'format'} ne 'html') { print "\n\n"; }

        # turn on expansion and remove the notice for both forms being printed
        $O{'expand'} = 'true';
        $O{'bothforms'} = 0;
}

# print all the matched results
&print_matches;

########################
#
# END Main Program
#
########################

########################
#
# POD Documentation
#
########################

=head1 NAME

expand-juniper.pl - A script to search and locate potential matching security policies for a target IP address.

=head1 SYNOPSIS

expand-juniper.pl [-d] [-e] [-E] [-F <format>] [-h] [-z <target zone>] -f <filename> -t <target ip> [-w] [-s] [-d]

This script requires the following Perl modules: Getopt::Easy, NetAddr::IP, Text::ASCIITable, Date::Format

=head1 DESCRIPTION

This script was designed to fill a gap in the reporting capabilities of the
Juniper ISG 2000 and Network Security Manager interfaces. The primary purpose
of this script is to compare a target IP address with policies detailed in a
Juniper security device (gathered from a copy of the configuration), and list
any policies that may potentially affect traffic to or from the particular
target IP. The script makes no assumptions about a default deny or allow
policy, it simply lists whether or not a rule could potentially affect traffic
to/from an IP address. The analysis of the information provided by this script
is left to the operator. This script could be used for either troubleshooting,
or for compliance auditing. 

=head1 USAGE

=over 10

=item C<-d>

Turns on debugging, which prints verbose output regarding what lines are
captured and which ones are not. (Optional)

=item C<-e>

Expand Groups. This option, when enabled, will expand the configured group and
address objects out to their full Classless Interdomain Route (CIDR) notations.
Warning, this option will usually create a much larger report. (Optional)

=item C<-E>

Will print both the compact and expanded forms of the results (ie both with and
without -e). Using this option causes the -e option to be ignored if used in
combination with -E. (Optional)

=item C<-F format>

Specifies the output format. The default is table. Supported formats are
'table', 'html', and 'csv'. Note that all output is currently sent to STDOUT.
(Optional)

==item C<-h>

Help. This option displays a quick help menu listing all the available options.
(Optional)

=item C<-z targetzone>

Denotes the security zone where the target IP is located. Using this option
will reduce the number of matches by eliminating policies that only match on a
source or destination of 'Any' outside of the target security zone. Using this
option is recommended. (Optional)

=item C<-f filename>

The location of the configuration file from the device. This should be either
an absolute path to the configuration file, or a path relative to the running
directory of the script. (Required)

=item C<-t targetobj>

This is the IP address/CIDR/hostname that will be used to match against the
various policies in the configuration. (Required)

The following formats are acceptable as targetobj:

        192.168.20.88                           Single IP address, assumes a /32 mask.
        192.168.20.0/24                         A CIDR notation subnet. Note that matches must contain the entire subnet.
        192.168.20.0/255.255.255.0              Full address notation. Note that matches must contain the entire subnet.
        0.0.0.0/0                               Represents all IP addresses. Will match all policies, regardless of zone.
        any                                     Alias for 0.0.0.0/0.
        fe80::/64                               IPv6

=item C<-w>

Matches <targetobj> or anything within <targetobj>.  

For example -t 192.168.10.0/24 -w will match a rule using specifically 192.168.10.99

=item C<-d>

Match <targetobj> only against policy destinations (default is both source and destination)

=item C<-s>

Match <targetobj> only against policy sources (default is both source and destination)



=back

=head1 AUTHOR

B<James Schneider> - jrschneider@csupomona.edu

Patch contributed by James M.

=head1 COPYRIGHT

Copyright James Schneider, January 2010
